#!/usr/bin/env python
# -*- coding: UTF-8 -*-

__version__ = "0.2.0"
__date__ = "2011-03-20"

Configuration=0x1;                      # USB configuration number
Interface=0x0;                          # USB interface number
EndpointIn=0x81;                        # USB endpoint address for input
EndpointOut=0x1;                        # USB endpoint address for output
Timeout=2000;                           # Timeout in ms
ReqLen=64;                              # Request length
Degree=u'C'                             # Degree Sign
QuestionCounts = 'L'                     # Request string for light data
QuestionTemp = 'T'                      # Request string for temperature data
QuestionID = 'i'                      # Request string for sensor id
MaskStatusCounts = int('10000000',2)     # Bitmask for light status bit
MaskDataCounts =  int('01111111',2)      # Bitmask for light data bits
MaskStatusTemp = int('00000111',2)      # Bitmask for temp status bits
Sleep = 1.0                             # Sleep time

LightmeterID=[{"VendorID":0x04d8,"ProductID":0x000c,"Version":'2.3'},{"VendorID":0x04d8,"ProductID":0xfcb7,"Version":'2.4'}]

import time
from datetime import datetime, timedelta
import sys
import io
from optparse import OptionParser

try:
    import usb.core
    
except ImportError:
    print """
    PyUSB 1.0 library is missing.
    Please install from http://sourceforge.net/projects/pyusb/files/
    """
    sys.exit(1)

#lightmeters = usb.core.find(idVendor=VendorID, idProduct=ProductID,find_all=True)

#for dev in lightmeters:
#    print dev

def toint(i):
    if i:
        return int(i)
    else:
        return None
        
def tofloat(f):
    if f:
        return float(f)
    else:
        return None
        
def tostr(s):
    if s:
        return str(s)
    else:
        return ''

def booltostr(b):
    if b:
        return 'ok'
    else:
        return 'err'
        
def strtobool(s):
    if s.lower()=='ok':
        return True
    elif s.lower()=='err':
        return False
    else:
        return None

class lighttime(datetime):
    def date23(self):
        return "%i.%i.%i"%(self.day,self.month,self.year)
    def time23(self):
        return "%02i:%02i:%02i"%(self.hour,self.minute,self.second)
    def line23(self):
        return self.date23()+';'+self.time23()
    def date24(self):
        return "%04i-%02i-%02i"%(self.year,self.month,self.day)
    def time24(self):
        return "%02i:%02i:%02i+%04i"%(self.hour,self.minute,self.second,self.microsecond/1000)
    def header24(self):
        return('Date,Time')
    def line24(self):
        return self.date24()+','+self.time24()

class lightmeter(object):
    """Class that representing a lightmeter."""
    
    def __init__(self,debug=False,update=1.0,filename=None,delimiter=',',buffering=0,\
                 latitude="",longitude="",station='',sensor='',number=0,verbose=False,
                 filehandler=None,version=None,temperature=None,tempstatus=None,
                 visual=None,infrared=None,status=None,counts=None,light=None,dev=None,
                 csvtype="2.4",):
        self.debug=debug
        self.update=update
        self.buffering=int(buffering)
        self.number = int(number)
        self.filename=filename
        self.delimiter=delimiter
        self.filehandler=filehandler
        self.latitude=latitude
        self.longitude=longitude
        self.station=station
        self.version=version
        self.temperature=temperature
        self.tempstatus=tempstatus
        self.counts=counts
        self.light=light
        self.visual=visual
        self.infrared=infrared
        self.status=status
        self.sensor=sensor
        self.verbose=verbose
        self.csvtype=csvtype
        self.dev = dev
        

    def __str__(self):
        
        return "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s"%(
                    tostr(self.station),tostr(self.sensor),tostr(self.latitude),tostr(self.longitude),
                    tostr(self.temperature),tostr(self.counts),tostr(self.visual),tostr(self.infrared),
                    tostr(self.light),booltostr(self.status)
                    )
    def header23(self):
        return  "# STATION: "+tostr(self.station)+'\n'+\
                "# SENSOR: "+tostr(self.sensor)+'\n'+\
                "# VERSION: "+tostr(self.version)+'\n'+\
                "# LATITUDE: "+ tostr(self.latitude).replace('.',',')+'\n'+\
                "# LONGITUDE: "+tostr(self.longitude).replace('.',',')+'\n'\
                "#Date;Time;Temperature;Grad;Counts;Visual;Infrared;Status"
                
    def line23(self):
        return "%s;°C;%s;%s;%s;%s;"%(tostr(self.temperature).replace('.',','),
                    tostr(self.counts),tostr(self.visual),tostr(self.infrared),booltostr(self.status))
    
    def header24(self):
        return 'Station,Sensor,Latitude,Longitude,Temperature,Counts,Visual,Infrared,Light,Status'
        
    def line24(self):
        return self.__str__()
        
    def __repr__(self):
        
        return "lightmeter(%s)"%self.__str__()
        
    def __del__(self):
        """Disconnect from lightmeter station."""
        self.dev = None

    def connect(self):
        """Connecting to lightmeter"""
        
        if self.debug:
            print "Searching for lightmeter",self.number
        
        nr = 0
        found = False
        for lmid in LightmeterID:
            devices= usb.core.find(idVendor=lmid['VendorID'], idProduct=lmid['ProductID'],find_all=True)
            
            if self.debug:
                if devices:
                    print "Found",devices
            
            for device in devices:
                if self.debug:
                    print "Found",nr,"searching for",self.number
                if nr == self.number and not found:
                    
                    if self.debug:
                        print "Connecting to",device
                    self._connect(device)
                    found = True
                nr += 1
        
    def _connect(self,device):
        """Connecting to lightmeter by device handler"""
        
        self.dev=device

        if self.debug:
            print "Connecting to lightmeter",self.dev
        
        try:
            self.dev.set_configuration(Configuration)
            if not self.sensor:
                self.readsensor()
            self.setversion()
            
        except usb.core.USBError:
            try:
                self.dev.detach_kernel_driver(Interface)
                self.dev.attach_kernel_driver(Interface)
            except usb.core.USBError:
                pass

        lt=lighttime(2010,03,20)
        if self.csvtype=='2.3':
            header= self.header23()
        else:
            header = lt.header24()+','+self.header24()
                
        if self.filename and not self.filehandler:
            if self.debug:
                print "Writing to file",self.filename
            self.filehandler=io.open(self.filename,'wb',buffering=int(self.buffering))
            self.filehandler.write(header+'\n')

                
        if self.verbose:
           print header
           
    def reset(self):
        """Resetting USB device"""
        
        if self.debug:
            print "Reseting",self.dev
        self.dev = None
        while not self.dev:
            time.sleep(Sleep)
            self.connect()

    def list(self):
        """Listing connected lightmeters."""
        
        nr = 0
        while nr <32:
            self.dev=None
            self.version=None
            self.sensor=None
            self.number=nr
            self.connect()
            if self.dev:
                print "%i:%s-%s"%(nr,self.version,self.sensor)
            nr += 1
            
            
                
    def write (self, buffer,timeout=Timeout):
        """Write to USB device"""
        return self.dev.write(EndpointOut,buffer,0,timeout)
    
    def read (self, numbytes=ReqLen,timeout=Timeout):
        """Read from USB device"""
        return self.dev.read(EndpointIn,numbytes,0,timeout)
        
    def getval(self,question):
        """Get value from USB device"""
        try:
            self.write(question)    
            return self.read()
        except usb.core.USBError:
            self.reset()
            return self.getval(question)
    
    def readsensor(self):
        """Read Serial Number"""
        req = self.getval(QuestionID)
        
        if req:
            snrlow,snrhigh = req
            if snrhigh <> 255 and snrlow <> 255:
                snr = snrhigh*256 + snrlow

                if snr < 10000:
                    self.sensor = snr + 910000
                else:
                    self.sensor= snr + 1000000
        else:
            self.sensor=None


    def setversion(self,version=None):
        if version:
            self.version=version
        else:
            if not self.sensor or int(self.sensor) < 910000:
                self.version="2.3"
            elif int(self.sensor) > 1000000:
                self.version="2.4"
            else:
                self.version="2.3L"

        
    def readcounts(self):
        """Read counts value"""
        req = self.getval(QuestionCounts)
        
        self.counts=None
        self.infrared=None
        self.visual=None
        self.light=None
        self.status=False
        
        if self.debug:
            print len(req),req
        
        if len(req) ==3 :           
            countslow, countshigh, valrange= req
            
        if len(req) ==7 : 
            countslow, countshigh, valrange, visuallow, visualhigh, infraredlow, infraredhigh = req
            self.visual = (visualhigh <<8) + visuallow
            self.infrared = (infraredhigh <<8) + infraredlow
       
        if len(req) ==3 or len(req) ==7 : 
            self.status = (countshigh & MaskStatusCounts) == 0               # Check if status bit == 0
            countsraw = ((countshigh & MaskDataCounts) << 8)  + countslow
            
            if valrange == 1:
                self.counts = countsraw * 120
            elif valrange == 2:
                self.counts = countsraw * 8
            elif valrange == 3:
                self.counts = countsraw * 4
            elif valrange == 4:
                self.counts = countsraw * 2
            elif valrange == 5:
                self.counts = countsraw

     
    def readtemp(self):
        """Read temperature"""
        req = self.getval(QuestionTemp)
        
        if len(req) == 2:
            templow, temphigh = req
        
            status = (templow & MaskStatusTemp) == 0            # Check if status bit == 0
            tempraw = ((temphigh << 8)  + templow ) >> 3        # Statusbits ausblenden
            temp = 625.0 * tempraw / 10000.0                    # Umrechnung von 1/16° zu 1/10°
        else:
            temp = -1 
            status = False
        
        self.temperature = temp
        self.tempstatus = status
        
    def reader(self):
        """Continously read from lightmeter"""
        dtupdate =  timedelta(seconds=self.update)
        dtmes = datetime.utcnow()

        
        while True:      
            dtnow = datetime.utcnow()
            dtuncalibrated = dtmes + dtupdate
            dtcalibrated = dtuncalibrated.replace(microsecond=(dtuncalibrated.microsecond)/100000*100000)
            dtsleep = dtcalibrated - dtnow
            sleep = dtsleep.seconds+(dtsleep.microseconds)/1000.0/1000.0
            if sleep <= self.update:
                time.sleep(sleep)          
            dtmes= datetime.utcnow()
            ltmes= lighttime(dtmes.year,dtmes.month,dtmes.day,
                             dtmes.hour,dtmes.minute,dtmes.second,
                             dtmes.microsecond)
            try:  
                self.readtemp()
                self.readcounts()
            except AttributeError:
                print "No lightmeter found"
                self.reset()
                continue
            
            if self.csvtype=='2.3':
                line = ltmes.line23()+';'+self.line23()
            else:
                line = ltmes.line24()+','+self.line24()
            
            if self.filehandler:
                self.filehandler.write(line+'\n')
                
            if self.verbose:
                print line

def main():
    """run lightmeter"""
    
    parser = OptionParser()
    parser.add_option("-l","--list", dest="list", default=False, action="store_true",
                                      help="list connected lightmeters")
    parser.add_option("-d","--debug", dest="debug", default=False, action="store_true",
                                      help="enable debug")
    parser.add_option("-c","--connect", dest="connect", default=False, action="store_true",
                                      help="connect to lightmeter")
    parser.add_option("-u","--usbnumber", dest="number", default=0, metavar="number",
                                      help="use lightmeter with usb number [%default]")
    parser.add_option("-f","--frequency", dest="update", default=1.0, metavar="seconds",
                                      help="update frequency in seconds [%default]")
    parser.add_option("-n","--station", dest="station", default="", metavar="id",
                                      help="station id")
    parser.add_option("-s","--sensor", dest="sensor", default="", metavar="snr",
                                      help="sensor serial number")
    parser.add_option("-o","--filename-out", dest="filename_out", default='', metavar="file",
                                      help="filename for data logging")
    parser.add_option("-a","--filename-auto", dest="filenameauto", default=False, action="store_true",
                                      help="Automatically generate filename")
    parser.add_option("-B","--buffering", dest="buffering", default=0, metavar="bytes",
                                      help="buffer file I/O [%default]")
    parser.add_option("-t","--type-out", dest="csvtype", default='2.4', metavar="type",
                                      help="version '2.3' or '2.4'"),
    parser.add_option("--lat", dest="latitude", default="", metavar="latitude",
                                      help="set latitude")
    parser.add_option( "--lon",dest="longitude", default="", metavar="longitude",
                                      help="set longitude")
    parser.add_option("-v", "--verbose",action="store_true", dest="verbose", default=False,
                                      help="be verbose")
    (options, args) = parser.parse_args()

                    
    if not options.connect and not options.filename_out and not options.list:
        parser.print_help()
        exit(-1)
             
    if options.filenameauto:
        dtnow = datetime.utcnow()
        datenow = "%04i_%02i%02i"%(dtnow.year,dtnow.month,dtnow.day)
        timenow = "%02i%02i%02i"%(dtnow.hour,dtnow.minute,dtnow.second)
        filename = options.filename_out+options.station+'_'+datenow+'_'+timenow+'.csv'
    else:
        filename = options.filename_out

    lm = lightmeter(debug=options.debug,
                    update=float(options.update),
                    station = options.station,
                    sensor=options.sensor,
                    filename= filename,
                    buffering=options.buffering,
                    latitude=options.latitude,
                    longitude=options.longitude,
                    number=int(options.number),
                    verbose=options.verbose,
                    csvtype=options.csvtype,
                    )        
    if options.list:
        lm.list()
    else:
        lm.connect()
        lm.reader()
           
           
if __name__ == "__main__":
    main()
        
        
    
    
